package handler

import (
	"github.com/DiracLee/dires-go/app/cmdline"
	"github.com/DiracLee/dires-go/app/database"
	"github.com/DiracLee/dires-go/app/payload"
	"github.com/DiracLee/dires-go/ds/dict"
	"github.com/DiracLee/dires-go/ds/list"
	"github.com/DiracLee/dires-go/ds/set"
	"github.com/DiracLee/dires-go/ds/sortedset"
	"github.com/DiracLee/dires-go/utils"
	"strconv"
	"time"
)

func init() {
	RegisterCommand(cmdline.CmdKeys, handleKeys, noPrepare, nil, 2)
	RegisterCommand(cmdline.CmdType, handleType, readFirstKey, nil, 2)
	RegisterCommand(cmdline.CmdExists, handleExists, readAllKeys, nil, -2)
	RegisterCommand(cmdline.CmdRename, handleRename, prepareRename, undoRename, 3)
	RegisterCommand(cmdline.CmdRenameNX, handleRenameNx, prepareRename, undoRename, 3)
	RegisterCommand(cmdline.CmdPersist, handlePersist, writeFirstKey, undoExpire, 2)
	RegisterCommand(cmdline.CmdExpire, handleExpire, writeFirstKey, undoExpire, 3)
	RegisterCommand(cmdline.CmdExpireAt, handleExpireAt, writeFirstKey, undoExpire, 3)
	RegisterCommand(cmdline.CmdPExpire, handlePExpire, writeFirstKey, undoExpire, 3)
	RegisterCommand(cmdline.CmdPExpireAt, handlePExpireAt, writeFirstKey, undoExpire, 3)
	RegisterCommand(cmdline.CmdTTL, handleTTL, readFirstKey, nil, 2)
	RegisterCommand(cmdline.CmdPTTL, handlePTTL, readFirstKey, nil, 2)
	RegisterCommand(cmdline.CmdDel, handleDel, writeAllKeys, undoDel, -2)
	RegisterCommand(cmdline.CmdFlushDB, handleFlushDB, noPrepare, nil, -1)
}

func handleKeys(db database.DB, args cmdline.CmdLine) payload.Payload {
	pattern := utils.CompilePattern(string(args[0]))
	result := make(cmdline.CmdLine, 0)
	db.ForEach(func(key string, val interface{}) bool {
		if pattern.IsMatch(key) {
			result = append(result, []byte(key))
		}
		return true
	})
	return payload.NewMultiBulkPayload(result)
}

func handleType(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	entity, exists := db.Get(key)
	if !exists {
		return payload.NewStatusPayload("none")
	}
	switch entity.Data.(type) {
	case []byte:
		return payload.NewStatusPayload("string")
	case list.List:
		return payload.NewStatusPayload("list")
	case dict.Dict:
		return payload.NewStatusPayload("hash")
	case set.Set:
		return payload.NewStatusPayload("set")
	case sortedset.SortedSet:
		return payload.NewStatusPayload("zset")
	}
	return &payload.UnknownErrPayload{}
}

// handleExists checks if a is existed in Storage
func handleExists(db database.DB, args cmdline.CmdLine) payload.Payload {
	result := int64(0)
	for _, arg := range args {
		key := string(arg)
		_, exists := db.Get(key)
		if exists {
			result++
		}
	}
	return payload.NewIntPayload(result)
}

func handleRename(db database.DB, args cmdline.CmdLine) payload.Payload {
	if len(args) != 2 {
		return payload.NewErrPayload("ERR wrong number of arguments for 'rename' command")
	}
	src := string(args[0])
	dest := string(args[1])

	entity, ok := db.Get(src)
	if !ok {
		return payload.NewErrPayload("ERR no such key")
	}
	rename(db, src, dest, entity)
	db.AddAOF(NamedCommand(cmdline.CmdRename, args...))
	return &payload.OkPayload{}
}

func handleRenameNx(db database.DB, args cmdline.CmdLine) payload.Payload {
	if len(args) != 2 {
		return payload.NewErrPayload("ERR wrong number of arguments for 'rename' command")
	}
	src := string(args[0])
	dest := string(args[1])

	_, ok := db.Get(dest)
	if ok {
		return payload.NewIntPayload(0)
	}

	entity, ok := db.Get(src)
	if !ok {
		return payload.NewErrPayload("ERR no such key")
	}
	rename(db, src, dest, entity)
	db.AddAOF(NamedCommand(cmdline.CmdRenameNX, args...))
	return &payload.OkPayload{}
}

func rename(db database.DB, src, dest string, entity *database.DataEntity) {
	expireTime, hasTTL := db.GetTTL(src)
	db.PutOrSet(dest, entity)
	db.Delete(src)
	if hasTTL {
		db.Persist(src) // clean src and dest with their ttl
		db.Persist(dest)
		db.SetKeyTTL(dest, expireTime)
	}
}

func undoRename(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	src := string(args[0])
	dest := string(args[1])
	return rollbackGivenKeys(db, src, dest)
}

func handlePersist(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	_, exists := db.Get(key)
	if !exists {
		return payload.NewIntPayload(0)
	}

	_, exists = db.GetTTL(key)
	if !exists {
		return payload.NewIntPayload(0)
	}

	db.Persist(key)
	db.AddAOF(NamedCommand("persist", args...))
	return payload.NewIntPayload(1)
}

func handleExpire(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])

	ttlArg, err := strconv.ParseInt(string(args[1]), 10, 64)
	if err != nil {
		return payload.NewErrPayload("ERR value is not an integer or out of range")
	}
	ttl := time.Duration(ttlArg) * time.Second

	_, exists := db.Get(key)
	if !exists {
		return payload.NewIntPayload(0)
	}

	expireAt := time.Now().Add(ttl)
	db.SetKeyTTL(key, expireAt)
	db.AddAOF(ExpireCommand(key, expireAt))
	return payload.NewIntPayload(1)
}

func handleExpireAt(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])

	raw, err := strconv.ParseInt(string(args[1]), 10, 64)
	if err != nil {
		return payload.NewErrPayload("ERR value is not an integer or out of range")
	}
	expireAt := time.Unix(0, raw*int64(time.Second))

	_, exists := db.Get(key)
	if !exists {
		return payload.NewIntPayload(0)
	}

	db.SetKeyTTL(key, expireAt)
	db.AddAOF(ExpireCommand(key, expireAt))
	return payload.NewIntPayload(1)
}

func handlePExpire(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])

	ttlArg, err := strconv.ParseInt(string(args[1]), 10, 64)
	if err != nil {
		return payload.NewErrPayload("ERR value is not an integer or out of range")
	}
	ttl := time.Duration(ttlArg) * time.Millisecond

	_, exists := db.Get(key)
	if !exists {
		return payload.NewIntPayload(0)
	}

	expireAt := time.Now().Add(ttl)
	db.SetKeyTTL(key, expireAt)
	db.AddAOF(ExpireCommand(key, expireAt))
	return payload.NewIntPayload(1)
}

func handlePExpireAt(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])

	raw, err := strconv.ParseInt(string(args[1]), 10, 64)
	if err != nil {
		return payload.NewErrPayload("ERR value is not an integer or out of range")
	}
	expireAt := time.Unix(0, raw*int64(time.Millisecond))

	_, exists := db.Get(key)
	if !exists {
		return payload.NewIntPayload(0)
	}

	db.SetKeyTTL(key, expireAt)
	db.AddAOF(ExpireCommand(key, expireAt))
	return payload.NewIntPayload(1)
}

func undoExpire(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	key := string(args[0])
	return []cmdline.CmdLine{
		TTLCommand(db, key),
	}
}

func handleTTL(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	_, exists := db.Get(key)
	if !exists {
		return payload.NewIntPayload(-2)
	}

	expireTime, exists := db.GetTTL(key)
	if !exists {
		return payload.NewIntPayload(-1)
	}
	ttl := expireTime.Sub(time.Now())
	return payload.NewIntPayload(int64(ttl / time.Second))
}

func handlePTTL(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	_, exists := db.Get(key)
	if !exists {
		return payload.NewIntPayload(-2)
	}

	expireTime, exists := db.GetTTL(key)
	if !exists {
		return payload.NewIntPayload(-1)
	}
	ttl := expireTime.Sub(time.Now())
	return payload.NewIntPayload(int64(ttl / time.Millisecond))
}

func handleDel(db database.DB, args cmdline.CmdLine) payload.Payload {
	keys := make([]string, len(args))
	for i, key := range args {
		keys[i] = string(key)
	}
	deleted := db.Delete(keys...)
	if deleted > 0 {
		db.AddAOF(NamedCommand(cmdline.CmdDel, args...))
	}
	return payload.NewIntPayload(int64(deleted))
}

func undoDel(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	keys := make([]string, len(args))
	for i, v := range args {
		keys[i] = string(v)
	}
	return rollbackGivenKeys(db, keys...)
}

// handleFlushDB removes all data in current Storage
func handleFlushDB(db database.DB, args cmdline.CmdLine) payload.Payload {
	db.Flush()
	db.AddAOF(NamedCommand(cmdline.CmdFlushDB, args...))
	return &payload.OkPayload{}
}
